﻿//--------------------------------------------------------------------------------------------------------------------------------------------------------
// <copyright file="SecurityService.cs">
//     Copyright © 2010 Oli Francis
//     This source code is subject to the terms and conditions of the MIT license. A copy of the license can be found in the license.txt 
//     file at the root of this distribution. By using this source code in any fashion, you are agreeing to be bound by the terms of the MIT License. 
//     You must not remove this notice from the software.
// </copyright>
// <author>Oli</author>
//-------------------------------------------------------------------------------------------------------------------------------------------------------

namespace TeamBrain.Domain.Services.Security
{
    #region Using Statements

    using System;

    using TeamBrain.Domain.Core;
    using TeamBrain.Domain.Core.Repositories;
    using TeamBrain.Domain.Model;
    using TeamBrain.Domain.Services.Configuration;
    using TeamBrain.Domain.Services.Email;
    using TeamBrain.Domain.Services.Exceptions;

    #endregion

    /// <summary>
    /// Exposes functionality around authentication and permissions
    /// </summary>
    public class SecurityService : ISecurityService
    {
        #region Private fields

        private readonly IUnitOfWork unitOfWork;
        private readonly IAuthenticationProvider authenticationProvider;
        private readonly IUserRepository userRepository;
        private readonly IPasswordHasher passwordHasher;
        private readonly IConfiguration configuration;
        private readonly IEmailService emailService;

        #endregion

        #region Constructors

        /// <summary>
        /// Initializes a new instance of the <see cref="SecurityService"/> class.
        /// </summary>
        public SecurityService(
            IUnitOfWork unitOfWork,
            IAuthenticationProvider authenticationProvider,
            IUserRepository userRepository,
            IPasswordHasher passwordHasher,
            IConfiguration configuration,
            IEmailService emailService)
        {
            this.unitOfWork = unitOfWork;
            this.authenticationProvider = authenticationProvider;
            this.userRepository = userRepository;
            this.passwordHasher = passwordHasher;
            this.configuration = configuration;
            this.emailService = emailService;
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Attempts to sign in a user with the given credentials
        /// </summary>
        public SignInOutcome AttemptSignIn(string email, string password, bool rememberMe)
        {
            SignInOutcome outcome;
            var user = this.userRepository.GetByEmailAddress(email);

            if (user == null)
            {
                outcome = SignInOutcome.CredentialsIncorrect;
            }
            else
            {
                if (user.Disabled)
                {
                    outcome = SignInOutcome.AccountDisabled;
                }
                else
                {
                    if (this.AccountIsLocked(user))
                    {
                        outcome = SignInOutcome.AccountLocked;
                    }
                    else
                    {
                        if (this.LockoutHasExpired(user))
                        {
                            this.ResetFailedSignInDetails(user);
                        }

                        var hash = this.passwordHasher.GetPasswordHash(user.Salt, password);

                        if (hash.Equals(user.PasswordHash))
                        {
                            outcome = SignInOutcome.Success;
                            this.authenticationProvider.SetAuthCookie(user.Id, rememberMe);

                            if (user.FailedSignInAttemptCount > 0)
                            {
                                this.ResetFailedSignInDetails(user);
                            }
                        }
                        else
                        {
                            user.FailedSignInAttemptCount++;
                            user.LastFailedSignInAttempt = DateTime.Now;
                            this.unitOfWork.Flush();

                            outcome = user.FailedSignInAttemptCount >= 5 ?
                                SignInOutcome.AccountLocked :
                                SignInOutcome.CredentialsIncorrect;
                        }                                    
                    }
                }
            }

            return outcome;
        }

        /// <summary>
        /// Creates an authentication cookie based on the given identity
        /// </summary>
        public void SignInActivatedUser(int userId)
        {
            this.authenticationProvider.SetAuthCookie(userId, false);
        }

        /// <summary>
        /// Generates a password reset token and sends it to the given address
        /// </summary>
        public void RequestPasswordReset(string email)
        {
            var user = this.userRepository.GetByEmailAddress(email);

            if (user == null)
            {
                throw new EmailAddressNotRecognisedException();
            }

            user.PasswordResetToken = Guid.NewGuid().ToString();
            user.PasswordResetTokenTimeIssued = DateTime.Now;
            this.unitOfWork.Flush();

            this.emailService.SendPasswordResetEmail(user);
        }

        /// <summary>
        /// Evaluates whether the given password reset token is valid
        /// </summary>
        public TokenStatus CheckPasswordResetToken(string token)
        {
            var result = TokenStatus.NotRecognised;

            var user = this.userRepository.GetByPasswordResetToken(token);

            if (user != null)
            {
                var threshold = DateTime.Now.Subtract(TimeSpan.FromHours(this.configuration.PasswordResetValidForHours));
                if (user.PasswordResetTokenTimeIssued.HasValue &&
                    user.PasswordResetTokenTimeIssued.Value > threshold)
                {
                    result = TokenStatus.Valid;
                }
                else
                {
                    result = TokenStatus.Expired;
                }
            }
            else
            {
                result = TokenStatus.NotRecognised;
            }

            return result;
        }

        #endregion

        #region Private Methods

        /// <summary>
        /// Resets the failed sign in details against a given user account.
        /// </summary>
        private void ResetFailedSignInDetails(User user)
        {
            user.FailedSignInAttemptCount = 0;
            user.LastFailedSignInAttempt = null;
            this.unitOfWork.Flush();
        }

        /// <summary>
        /// Evaluates whether a given user account is locked
        /// </summary>
        private bool AccountIsLocked(User user)
        {
            var threshold = this.GetLockoutThreshold();

            return user.FailedSignInAttemptCount > this.configuration.SignInAttemptsBeforeLockout &&
                   user.LastFailedSignInAttempt.HasValue && 
                   user.LastFailedSignInAttempt.Value >= threshold;
        }

        /// <summary>
        /// Evaluates whether the account lockout period has expired for a given user
        /// </summary>
        private bool LockoutHasExpired(User user)
        {
            var threshold = this.GetLockoutThreshold();

            return user.FailedSignInAttemptCount > 0 && 
                   user.LastFailedSignInAttempt.HasValue &&
                   user.LastFailedSignInAttempt.Value < threshold;
        }

        /// <summary>
        /// Gets the lockout threshold.
        /// </summary>
        private DateTime GetLockoutThreshold()
        {
            return DateTime.Now.Subtract(TimeSpan.FromMinutes(this.configuration.MinutesUntilLockoutExpires));
        }

        #endregion
    }
}